/**
 * ControlPoint.java Created on : 28 juin 07
 */
package fr.limsi.seven;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.util.Vector;

import javax.swing.JPanel;

/**
 * @author Camille
 */
public class ControlPoint extends JPanel {

	/**
	 * Auto-generated serial version UID number
	 */
	private static final long serialVersionUID = -3644405435684797268L;

	/**
	 * Control point type integer constant
	 */
	public static final int HORIZONTAL = 0;

	/**
	 * Control point type integer constant
	 */
	public static final int VERTICAL = 1;

	/**
	 * Control point default size
	 */
	public static final int DEFAULT_SIZE = 8;

	/**
	 * Control point margins
	 */
	public static final int MARGINS = 1;

	/**
	 * Control point type
	 */
	protected int type;

	/**
	 * Associated {@link Annotation}
	 */
	protected Annotation annotation;

	/**
	 * Linked control points that must follow this control point movements
	 */
	protected Vector<ControlPoint> followers;

	/**
	 * Linked control points that must be avoided
	 */
	protected Vector<ControlPoint> avoiders;

	/**
	 * Relative X position on the {@link Annotation}
	 */
	protected float relativeXPosition;

	/**
	 * Relative Y position on the {@link Annotation}
	 */
	protected float relativeYPosition;

	/**
	 * Control point display indicator
	 */
	protected boolean displayed;

	/**
	 * Mouse X coordinate
	 */
	protected int mouseX;

	/**
	 * Mouse Y coordinate
	 */
	protected int mouseY;

	/**
	 * Default constructor
	 * 
	 * @param annotation
	 *            the associated {@link Annotation}
	 * @param type
	 *            a control point type integer constant
	 */
	public ControlPoint(Annotation annotation, int type) {
		this.type = type;
		this.annotation = annotation;

		followers = new Vector<ControlPoint>();
		avoiders = new Vector<ControlPoint>();

		displayed = false;

		buildInteractions();

		setOpaque(false);
	}

	/**
	 * Creates and sets the control point interactions
	 */
	protected void buildInteractions() {
		MouseAdapter mA = new MouseAdapter() {

			@Override
			public void mouseEntered(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
			}

			@Override
			public void mouseExited(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			}

			@Override
			public void mousePressed(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));

				mouseX = e.getLocationOnScreen().x;
				mouseY = e.getLocationOnScreen().y;
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			}

			@Override
			public void mouseDragged(MouseEvent e) {

				switch (type) {
				case HORIZONTAL:
					if (canMoveH(getLocationOnScreen().x + (e.getLocationOnScreen().x - mouseX))) {
						relativeXPosition = ((float) ((getLocationOnScreen().x + (e.getLocationOnScreen().x - mouseX)) - annotation.getLocationOnScreen().x)) / (float) annotation.getWidth();

						for (ControlPoint follow : followers) {
							follow.setRelativeXPosition(relativeXPosition);
						}
					}
					break;

				case VERTICAL:
					if (canMoveV(getLocationOnScreen().y + (e.getLocationOnScreen().y - mouseY))) {
						relativeYPosition = ((float) ((getLocationOnScreen().y + (e.getLocationOnScreen().y - mouseY)) - annotation.getLocationOnScreen().y)) / (float) annotation.getHeight();

						for (ControlPoint follow : followers) {
							follow.setRelativeYPosition(relativeYPosition);
						}
					}
					break;
				}

				mouseX = e.getLocationOnScreen().x;
				mouseY = e.getLocationOnScreen().y;

				annotation.update();
			}
		};

		addMouseListener(mA);
		addMouseMotionListener(mA);
	}

	/**
	 * Verifies that a control point can be moved horizontally
	 * 
	 * @param x
	 *            the goal coordinate
	 * @return true if the control point can be moved, or else false
	 */
	protected boolean canMoveH(int x) {

		if (type == VERTICAL) {
			return false;
		}

		for (ControlPoint avoid : avoiders) {
			if (Math.abs(avoid.getLocationOnScreen().x - x) <= 1) {
				return false;
			}
		}

		if (x > annotation.getLocationOnScreen().x && x + DEFAULT_SIZE < annotation.getLocationOnScreen().x + annotation.getWidth() - Annotation.MARGINS) {
			return true;
		}
		else {
			return false;
		}
	}

	/**
	 * Verifies that a control point can be moved vertically
	 * 
	 * @param y
	 *            the goal coordinate
	 * @return true if the control point can be moved, or else false
	 */
	protected boolean canMoveV(int y) {

		if (type == HORIZONTAL) {
			return false;
		}

		for (ControlPoint avoid : avoiders) {
			if (Math.abs(avoid.getLocationOnScreen().y - y) <= 1) {
				return false;
			}
		}

		if (y > annotation.getLocationOnScreen().y && y + DEFAULT_SIZE < annotation.getLocationOnScreen().y + annotation.getHeight() - Annotation.MARGINS) {
			return true;
		}
		else {
			return false;
		}
	}

	@Override
	protected void paintComponent(Graphics g) {

		super.paintComponent(g);

		if (displayed) {
			Graphics2D g2 = (Graphics2D) g;

			g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);

			g2.setPaint(Color.BLACK);
			g2.fill(new Ellipse2D.Float(MARGINS, MARGINS, getWidth() - (2 * MARGINS), getHeight() - (2 * MARGINS)));
		}
	}

	/**
	 * Setter for the displayable state of the control point
	 * 
	 * @param displayed
	 *            the state to be set
	 */
	public void setDisplayed(boolean displayed) {
		this.displayed = displayed;
	}

	/**
	 * Setter for the control point type
	 * 
	 * @param type
	 *            the type constant integer to be set
	 */
	public void setType(int type) {
		this.type = type;
	}

	/**
	 * Adds a control point to the avoid list
	 * 
	 * @param avoid
	 *            the control point to be avoided
	 */
	public void addAvoider(ControlPoint avoid) {
		avoiders.add(avoid);
	}

	/**
	 * Adds a control point to the follow list
	 * 
	 * @param avoid
	 *            the control point that must follow this control point movements
	 */
	public void addFollower(ControlPoint follow) {
		followers.add(follow);
	}

	/**
	 * Getter for the control point relative x postion on the {@link Annotation}
	 * 
	 * @return the control point relative x position (i.e a ratio ranging between 0 and 1)
	 */
	public float getRelativeXPosition() {
		return relativeXPosition;
	}

	/**
	 * Setter for the control point relative x postion on the {@link Annotation}
	 * 
	 * @param relativeXPosition
	 *            the relative x position to be set (i.e a ratio ranging between 0 and 1)
	 */
	public void setRelativeXPosition(float relativeXPosition) {
		this.relativeXPosition = relativeXPosition;
	}

	/**
	 * Getter for the control point relative y postion on the {@link Annotation}
	 * 
	 * @return the control point relative y position (i.e a ratio ranging between 0 and 1)
	 */
	public float getRelativeYPosition() {
		return relativeYPosition;
	}

	/**
	 * Setter for the control point relative y postion on the {@link Annotation}
	 * 
	 * @param relativeXPosition
	 *            the relative y position to be set (i.e a ratio ranging between 0 and 1)
	 */
	public void setRelativeYPosition(float relativeYPosition) {
		this.relativeYPosition = relativeYPosition;
	}

	/**
	 * Getter for the control point relative location on the {@link Annotation}
	 * 
	 * @return a {@link Point2D} containing the x and the y relative position (i.e a ratios ranging between 0 and 1)
	 */
	public Point2D.Float getRelativePosition() {
		return new Point2D.Float(relativeXPosition, relativeYPosition);
	}

	/**
	 * Setter for the control point relative location on the {@link Annotation}
	 * 
	 * @param relativeXPosition
	 *            the relative x position to be set (i.e a ratio ranging between 0 and 1)
	 * @param relativeYPosition
	 *            the relative y position to be set (i.e a ratio ranging between 0 and 1)
	 */
	public void setRelativePosition(float relativeXPosition, float relativeYPosition) {
		this.relativeXPosition = relativeXPosition;
		this.relativeYPosition = relativeYPosition;
	}
}
